<?php
App::uses('ModelBehavior', 'Model/Behavior');
class LilUploadBehavior extends ModelBehavior {
	
	var $__defaultSettings = array(
		'enabled' => true,
		'safeUpload'    => true,     // turn safe upload off use tmp file generated on flight
		'fields' => array(),
		'pathReplacements' => array(),
		'_setupError' => false
	);
	
	var $__defaults = array(
		'enabled'       => true,
		'dirField'      => 'dir',
		'titleField'    => 'filename_orig', // this is an original file name before applying conversion
		'mimeField'     => 'mimetype',
		'sizeField'     => 'size',
		'extField'      => 'ext',
		'widthField'    => 'width',
		'heightField'   => 'height',
		'checksumField' => 'checksum',
		
		'allowedMime' => '*', //array('image/jpeg', 'image/pjpeg', 'image/gif', 'image/png'),
		'allowedExt' => '*', //array('jpg','jpeg','gif','png'),
		'allowedSize' => '8', // '*' for no limit (in any event limited by php settings)
		'allowedSizeUnits' => 'MB', 
		'overwriteExisting' => false,
		'mustUploadFile' => false,
		// Dynamic path/file names 
		// The constants DS, APP, WWW_ROOT can be used if wrapped in {}
		// To use a variable, wrap in {} if the var is not defined during setup it is assumed to be the name
		// of a field in the submitted data
		'baseDir'     => '{APP}uploads{DS}',
		'dirFormat'   => '{$class}{DS}{$foreign_id}',  // include {$baseDir} to have absolute paths
		'fileFormat'  => '{$filename}_{$description}', // include {$dir} to store the dir & filename in one field
		//'titleFormat' => '{$full_name}',             //leave empty for unsanitized version
	);
	
	function setup(&$model, $config = array()) {
		$settings = array_merge($this->__defaultSettings, $config);
		$this->settings[$model->alias] = $settings;
		
		$this->__resetReplace($model);
		
		if (empty($this->settings[$model->alias]['fields'])) {
			// compatibility
			$transfer_settings = array();
			$field_name = isset($this->settings[$model->alias]['fileField']) ? $this->settings[$model->alias]['fileField'] : 'filename';

			$this->settings[$model->alias]['fields'][$field_name] = array_merge($this->__defaults, $this->settings[$model->alias]);
			unset($this->settings[$model->alias]['fields'][$field_name]['fields']);
			unset($this->settings[$model->alias]['fields'][$field_name]['pathReplacements']);
			unset($this->settings[$model->alias]['fields'][$field_name]['_setupError']);
		}
		
		foreach ((array)$this->settings[$model->alias]['fields'] as $field => $settings) {
			$this->settings[$model->alias]['fields'][$field] = array_merge(
				$this->__defaults,
				$this->settings[$model->alias]['fields'][$field]
			);
			$field_settings = $this->settings[$model->alias]['fields'][$field];
			
			$path = $this->__replacePseudoConstants($model, $field_settings['baseDir']);
			
			if ($this->__checkBaseDir($path)) {
				$this->settings[$model->alias]['fields'][$field]['baseDir'] = $path;
			} else {
				trigger_error(sprintf('Base directory "%s" either isn\'t writable or cannot be created. ', $path), E_USER_WARNING);
				$this->settings[$model->alias]['enabled'] = false;
				$this->settings[$model->alias][$field]['_setupError'] = true;
			}
		}
		if (!$this->settings[$model->alias]['enabled']) {
			return;
		}
		
		$this->setupUploadValidations($model);
	}
	
	function setBaseDir(&$model, $path, $field = null) {
		if (empty($field)) {
			if (isset($this->settings[$model->alias]['fileField'])) {
				$field = $this->settings[$model->alias]['fileField'];
			} else {
				$field = 'filename';
			}
		}
		if ($this->__checkBaseDir($path)) {
			$this->settings[$model->alias]['fields'][$field]['baseDir'] = $path;
			return true;
		} else return false;
	}
	
	function __checkBaseDir($path) {
		if (!file_exists($path)) {
			App::uses('Folder', 'Utility');
			new Folder($path, true);
			if (!file_exists($path)) {
				return false;
			}
		} elseif (!is_writable($path)) {
			return false;
		}
		return true;
	}

	function enableUpload(&$model, $enable = null, $field = null) {
		$ret = $this->settings[$model->alias]['enabled'];
		if ($enable !== null) {
			if (empty($field)) {
				$ret = $this->settings[$model->alias]['enabled'] = $enable;
			} else {
				$ret = $this->settings[$model->alias]['fields'][$field]['enabled'] = $enable;
			}
		}
		return $ret;
	}
	
	function setSafeUpload(&$model, $safe = null) {
		$ret = $this->settings[$model->alias]['safeUpload'];
		if ($safe !== null) {
			$ret = $this->settings[$model->alias]['safeUpload'] = $safe;
		}
		return $ret;
	}
	
	function getLilUploadSettings(&$model) {
		return $this->settings[$model->alias];
	}
	
	function addReplace(&$model, $find, $replace = '') {
		$this->settings[$model->alias]['pathReplacements'][$find] = $replace;
	}
	
	function beforeDelete(&$model) {
		extract($this->settings[$model->alias]);
		if (!$enabled) {
			return true;
		}
		
		foreach ((array)$this->settings[$model->alias]['fields'] as $field => $field_settings) {
			if ($model->hasField($field_settings['dirField'])) {
				$data     = $model->read(array($field_settings['dirField'], $field));
				$dir      = $data[$model->alias][$field_settings['dirField']];
				$filename = $data[$model->alias][$field];
				$filename = $dir . DS . $filename;
			} else if ($model->hasField($field)) {
				$filename = $model->field($field);
			} else {
				// wasnt saved in a field
				return true;
			}
	
			if (!empty($filename) && 
				file_exists($field_settings['baseDir'] . $filename) && 
				!unlink($field_settings['baseDir'] . $filename)
			) {
				return false;
			}
		}
       	return true;
 	}

	function beforeSave(&$model) {
		if (!$this->settings[$model->alias]['enabled']) {
			return true;
		}
		$result = $this->processUpload($model, $model->data[$model->alias]);
		return $result;
	}

	function checkUploadSetup(&$model, $fieldData, $field) {
		$fieldData = $fieldData[$field];
		
		if (!empty($this->settings[$model->alias][$field]['_setupError'])) {
			return false;
		}
		if (!$this->settings[$model->alias]['enabled'] || !$this->settings[$model->alias]['fields'][$field]['enabled']) {
			return true;
		}
		
		// dont process upload when no uploaded file exist
		if (!is_array($fieldData)) {
			$this->enableUpload($model, false, $field);
		}
		return true;
	}

	function checkUploadError(&$model, $fieldData, $field) {
		$fieldData = $fieldData[$field];
		
		if (!$this->settings[$model->alias]['enabled'] || !$this->settings[$model->alias]['fields'][$field]['enabled'] || !empty($this->settings[$model->alias]['fields'][$field]['_setupError']) || !is_array($fieldData)) {
			return true;
		}
		
		if ($this->settings[$model->alias]['fields'][$field]['mustUploadFile'] && (empty($fieldData['size']) || !empty($fieldData['error']) || $fieldData['error'] != UPLOAD_ERR_OK)) {
			return false;
		}
		return true;
	}
	
	function checkUploadExist(&$model, $fieldData, $field) {
		$fieldData = $fieldData[$field];
		
		if (!$this->settings[$model->alias]['enabled'] || !$this->settings[$model->alias]['fields'][$field]['enabled'] || !empty($this->settings[$model->alias]['fields'][$field]['_setupError']) || !is_array($fieldData)) {
			return true;
		}
		
		if (empty($fieldData['name'])) {
			return false;
		}
		return true;
	}

	function checkUploadMime(&$model, $fieldData, $field) {
		$fieldData = $fieldData[$field];
		
		if (!$this->settings[$model->alias]['enabled'] || !$this->settings[$model->alias]['fields'][$field]['enabled'] || !empty($this->settings[$model->alias]['fields'][$field]['_setupError']) || empty($fieldData['type'])) {
			return true;
		}
		
		if (is_array($this->settings[$model->alias][$field]['allowedMime'])) {
			if (in_array($fieldData['type'], $this->settings[$model->alias][$field]['allowedMime'])) {
				return true;
			}
		} elseif ($this->settings[$model->alias][$field]['allowedMime'] == $fieldData['type']) {
			return true;
		}
		return false;
	}

	function checkUploadExt(&$model, $fieldData, $field) {
		$fieldData = $fieldData[$field];
		
		if (!$this->settings[$model->alias]['enabled'] || !$this->settings[$model->alias]['fields'][$field]['enabled'] || !empty($this->settings[$model->alias]['fields'][$field]['_setupError']) || empty($fieldData['name'])) {
			return true;
		}
		
		$info = pathinfo($fieldData['name']);
		$fileExt = isset($info['extension']) ? strtolower($info['extension']) : '';
		if (is_array($this->settings[$model->alias]['fields'][$field]['allowedExt'])) {
			if (in_array($fileExt, $this->settings[$model->alias]['fields'][$field]['allowedExt'])) {
				return true;
			}
		} elseif ($this->settings[$model->alias]['fields'][$field]['allowedExt'] == $fileExt) {
			return true;
		}
		return false;
	}

	function checkUploadSize(&$model, $fieldData, $field) {
		$fieldData = $fieldData[$field];
		
		if (!$this->settings[$model->alias]['enabled'] || !$this->settings[$model->alias]['fields'][$field]['enabled'] || !empty($this->settings[$model->alias]['fields'][$field]['_setupError']) || empty($fieldData)) {
			return true;
		}
		
		$factor = 1;
		switch ($this->settings[$model->alias]['fields'][$field]['allowedSizeUnits']) {
			case 'KB':
				$factor = 1024;
			case 'MB':
				$factor = 1024 * 1024;
		}
		
		if (empty($fieldData['size']) && file_exists($fieldData['tmp_name'])) {
			$fieldData['size'] = filesize($fieldData['tmp_name']);
		}
		if (isset($fieldData['size']) && $fieldData['size'] < ($this->settings[$model->alias]['fields'][$field]['allowedSize'] * $factor)) {
			return true;
		}
		return false;
	}	
	
	function processUpload(&$model, &$data) {
		$ret = true;
		foreach ((array)$this->settings[$model->alias]['fields'] as $field => $field_settings) {
			if (!$this->_processUpload($model, $field, $data)) $ret = false;
		}
		return $ret;
	}

	function setupUploadValidations(&$model) {
		foreach ((array)$this->settings[$model->alias]['fields'] as $field => $field_settings) {
			if (isset($model->validate[$field])) {
				$existingValidations = $model->validate[$field];
				if (!is_array($existingValidations)) {
					$existingValidations = array($existingValidations);
				}	
			} else {
				$existingValidations = array();
			}
			
			$validations['uploadSetup'] = array(
				'rule'    => array('checkUploadSetup', $field),
				'message' => __d('lil', 'Upload not possible. There is a problem with the setup on the server, more info available in the logs.')	
			);
			$validations['uploadError'] = array(
				'rule'    => array('checkUploadError', $field),
				'message' => __d('lil', 'An error was generated during the upload.')
			);

			if ($field_settings['allowedMime'] != '*') {
				if (is_array($field_settings['allowedMime'])) {
					$allowedMimes = implode(',', $field_settings['allowedMime']);
				} else {
					$allowedMimes = $field_settings['allowedMime'];
				}
				$validations['uploadMime'] = array(
					'rule' => array('checkUploadMime', $field),
					'message' => sprintf(__d('lil', 'The submitted mime type is not permitted, only %s permitted.'), $allowedMimes)
				);
			}
			
			if ($field_settings['allowedExt'] != '*') {
				if (is_array($field_settings['allowedExt'])) {
					$allowedExts = implode(',', $field_settings['allowedExt']);
				} else {
					$allowedExts = $field_settings['allowedExt'];
				}
				$validations['uploadExt'] = array(
					'rule'    => array('checkUploadExt', $field),
					'message' => sprintf(__d('lil', 'The submitted file extension is not permitted, only %s permitted.'), $allowedExts)
				);
			}
			
			if ($field_settings['mustUploadFile']) {
				$validations['uploadExist'] = array(
					'rule'    => array('checkUploadExist', $field),
					'message' => __d('lil', 'File was not uploaded.')
				);
			}
			$validations['uploadSize'] = array(
				'rule'    => array('checkUploadSize', $field),
				'message' => sprintf(__d('lil', 'The file uploaded is too big, only files less than %1$s %2$s permitted.'), $field_settings['allowedSize'], $field_settings['allowedSizeUnits'])	
			);
			
			$model->validate[$field] = array_merge($validations, $existingValidations); //Run the behavior validations first.
		}
	}

	function _getFilename($model, $string) {
		if (strpos($string, '{') === false) {
			return Inflector::underscore(preg_replace('@[^\p{L}0-9]@u', '', $string));
		}
		$ret = $this->__replacePseudoConstants($model, $string);
		return $ret;
	}

	function _getPath($model, $field, $path) {
		if (empty($path)) $path = '';
		
		if (strpos($path, '{') === false) {
			return $path;
		}
		if (!empty($path)) {
			$path = $this->__replacePseudoConstants($model, $path);
		}
		
		// try to create target folder
		App::uses('Folder', 'Utility');
		new Folder($this->settings[$model->alias]['fields'][$field]['baseDir'] . $path, true);
		return $path;
	}

	function _processUpload(&$model, $field, &$data, $direct = false) {
		// Double check for upload start
		if(!isset($data[$field])) {
			if ($direct) {
				trigger_error(sprintf(__d('lil', 'The method processUpload has been explicitly called but the filename field (%s) is not present in the submitted data. '), $field), E_USER_WARNING);
				return false;
			}
			return true; 
		}
		
		// Double check for upload end
		if (isset($data[$field]) && is_array($data[$field])) {
			if (!empty($data[$field]['name'])) {
				if (method_exists($model, 'beforeProcessUpload')) {
					if (!$model->beforeProcessUpload($data, $field, $direct)) {
						return false;
					}
				}
				
				$this->__resetReplace($model);
				
				// Get file path
				$info = pathinfo($data[$field]['name']);
				$extension = (empty($info['extension'])) ? '' : $info['extension'];
				$filename = (empty($info['filename'])) ? '' : $info['filename'];
				
				$dir = $this->_getPath($model, $field, $this->settings[$model->alias]['fields'][$field]['dirFormat']);
				$this->addReplace($model, '{$dir}', $dir);
		
				// Get filename
				App::uses('Sanitize', 'Utility');
				
				$f_name = Sanitize::paranoid($filename, array('_', '-'));
				$f_ext  = Sanitize::paranoid($extension, array('_', '-'));
				$f_full = $f_name . ((!empty($f_ext)) ? '.' . $f_ext : '');
				
				$this->addReplace($model, '{$filename}', $f_name);
				$this->addReplace($model, '{$full_name}', $f_full);
				$this->addReplace($model, '{$extension}', $f_ext);
				
				// ability to store original filename (unsanitized) in a separate field
				if (empty($this->settings[$model->alias]['fields'][$field]['titleFormat'])) {
					$data[$this->settings[$model->alias]['fields'][$field]['titleField']] = $filename . ((!empty($extension)) ? '.' . $extension : '');
				} else {
					$data[$this->settings[$model->alias]['fields'][$field]['titleField']] = $this->__replacePseudoConstants($model, $this->settings[$model->alias]['fields'][$field]['titleFormat']);
				}
				
				// this is a sanitized filename field
				$filename = $this->_getFilename($model, $this->settings[$model->alias]['fields'][$field]['fileFormat']);
				$data[$field]['name'] = $filename;
				
				// Create save path
				if (!empty($dir)) $dir = $this->__includeTrailingPathDelimiter($dir);
				if (!empty($baseDir)) $baseDir = $this->__includeTrailingPathDelimiter($baseDir);
				
				$saveAs = $dir . $filename;
				
				// Check if file exists
				if(file_exists($this->settings[$model->alias]['fields'][$field]['baseDir'] . $saveAs)) {
					if($this->settings[$model->alias]['fields'][$field]['overwriteExisting']) {
						if(!unlink($this->settings[$model->alias]['fields'][$field]['baseDir'] . $saveAs)) {
							trigger_error(sprintf(__d('lil', 'The file %s already exists and cannot be deleted.'), $saveAs), E_USER_WARNING);
							return false;
						}
					} else {
						$count = 2;
						while(file_exists($this->settings[$model->alias]['fields'][$field]['baseDir'] . $dir . $filename . '_' . $count . '.' . $extension)) {
							$count++;
						}
						$data[$field]['name'] = $filename . '_' . $count . '.' . $extension;
						$saveAs = $dir . $filename . '_' . $count . '.' . $extension;
					}
				}
					
				// Attempt to move uploaded file
				$dest_file = $this->settings[$model->alias]['fields'][$field]['baseDir'] . $saveAs;
				if(!$this->__moveUploadedFile($model, $data[$field]['tmp_name'], $dest_file)) {
					trigger_error('Couldn\'t move the uploaded file. ', E_USER_WARNING);
					return false;
				}
				
				// Update model data
				$data[$this->settings[$model->alias]['fields'][$field]['dirField']] = $dir;
				
				if (!empty($data[$field]['type'])) {
					$data[$this->settings[$model->alias]['fields'][$field]['mimeField']] = $data[$field]['type'];
				}
				
				if (empty($data[$field]['size']) && file_exists($dest_file)) {
					$data[$field]['size'] = filesize($dest_file);
				}
				if (!empty($data[$field]['size'])) {
					$data[$this->settings[$model->alias]['fields'][$field]['sizeField']] = $data[$field]['size'];
				}
				$data[$field] = $data[$field]['name'];
				$data[$this->settings[$model->alias]['fields'][$field]['extField']] = $info['extension'];
				$data[$this->settings[$model->alias]['fields'][$field]['checksumField']] = md5_file($this->settings[$model->alias]['fields'][$field]['baseDir'] . $saveAs);
				
				if (in_array(strtolower($info['extension']), array('gif', 'jpg', 'png', 'bmp'))) {
					$image_info = getimagesize($this->settings[$model->alias]['fields'][$field]['baseDir'] . $saveAs);
					$data[$this->settings[$model->alias]['fields'][$field]['widthField']] = $image_info[0];
					$data[$this->settings[$model->alias]['fields'][$field]['heightField']] = $image_info[1];
				}
				
				
				if (method_exists($model, 'afterProcessUpload')) {
					$model->afterProcessUpload($data, $field, $direct);
				}
			} else {
				// if no file uploaded
				unset($data[$field]);
			}
		}
		return true;
	}
	
	function __moveUploadedFile($model, $source, $dest) {
		if ($this->settings[$model->alias]['safeUpload']) {
			return move_uploaded_file($source, $dest);
		} else {
			if ($ret = file_put_contents($dest, file_get_contents($source))) {
				unlink($source);
			}
			return $ret;
		}
	}
	
	function __includeTrailingPathDelimiter($path) {
		if (substr($path, -1, 1) != DS) return $path . DS;
		return $path;
	}
	
	function __resetReplace($model) {
		$this->settings[$model->alias]['pathReplacements'] = array();
		
		$this->addReplace($model, '{WWW_ROOT}', WWW_ROOT);
		$this->addReplace($model, '{APP}' , APP);
		$this->addReplace($model, '{DS}', DS);
	}

	function __replacePseudoConstants($model, &$string) {
		$this->addReplace($model, '{HASH}', uniqid(''));
		
		preg_match_all('@{\$([^{}]*)}@', $string, $r);
		foreach ($r[1] as $i => $match) {
			if (!isset($this->settings[$model->alias]['pathReplacements'][$r[0][$i]])) { 
				if (isset($$match) && !is_object($$match)) {
					$this->addReplace($model, $r[0][$i], $$match);
				} elseif (isset($model->data[$model->alias][$match])) {
					$this->addReplace($model, $r[0][$i], $model->data[$model->alias][$match]);
				} else {
					trigger_error(sprintf('Cannot replace "%1$s" as the variable "$%2$s" cannot be determined ', $match, $match), E_USER_WARNING);
				}
			}
		}
		$markers = array_keys($this->settings[$model->alias]['pathReplacements']);
		$replacements = array_values($this->settings[$model->alias]['pathReplacements']);
		return str_replace($markers, $replacements, $string);
	}
}